Multiplayer Tutorial

by S. B., aka SF (and SFSW), aka Vice (that StarWraith guy)

_______________________________________________________________

Welcome to the DarkBasic multiplayer tutorial. This document and the associated source code are intended to be compatible with both DarkBasic Version 1.X and DarkBasic Professional. The intent of this information is to help you design a multiplayer system for your games, including full use of the command set for real-time games, such as first person shooters and flight simulations. This tutorial also focuses on peer-to-peer connections, as this is generally the preferred method for gaming on a wide variety of systems and connection speeds (it's also the mode that I've found offers the most stability and functionality). While this tutorial covers one possible way to use multiplayer, there are numerous options to perform the same function. And even though this is fairly basic in concept, a solid working knowledge of the DarkBasic programming environment is strongly recommended before attempting to implement the routines shown here. The last few sections in this tutorial cover some fairly advanced concepts for packet exchanges and player movement prediction, but it's what some of the big name game maker's use, so it's important information to cover. I've tried to make this tutorial as easy to understand as possible, but I'm dealing with a complex subject. Multiplayer is a relatively advanced concept in programming, even with the easy-to-use DarkBasic command set. So if you find yourself not knowing how to accomplish some of what is discussed in this tutorial, you may want to review the help examples that come with DarkBasic as they are the best way to learn the language and concepts better. Also, there are much more complex ways of doing the tasks described here which add to accuracy and gameplay, but this will give you a basic model of techniques to use for good results.


Section 1: The Basics

The multiplayer commands found in DarkBasic allow you to exchange data (packets) over an internet connection, local area network (LAN), or other cable connection (for this tutorial, we will focus on just using the internet and LAN options). You can use this data to exchange scene information between players. Scene data can include player position/direction, weapon state, and other miscellaneous information. Your goal as the game designer will be to calibrate a scene so it is accurate for every player in the game. The speed of each player's connection will determine just how accurate the scene will be for that player.

Here are some basic concepts for designing a multiplayer system:
- Establish Connection Types
- Select Desired Connection and IP Address
- Decide Whether User is Host or Client
- If Host, Find Game, if Client, Join Game
- Exchange Name/Gamestate Data as Players Joins
- Keep a Running List of Current Players
- Continually Run Receiving Routine for New Packets
- Proceed with Desired Game Progression
- Check for Lost/Disconnected Players
- Check for Latency (Lag), Adjust as Needed

Now, let's go over some of these concepts in detail and how they will work with your game's structure and concept. Keep in mind that these elements can be moved around, looped as needed, and modified to adapt to any pattern you desire.


Section 2: Getting Started

Once you have a basic working engine for your game, it is best to continue development with multiplayer gameplay in mind. To do this, you will want to make sure all scene data is in easy to manage variables, is not randomly generated, and has a set speed (frame rate, model movement, etc) that makes movement prediction easier (which we'll review later on).

Once you're ready to introduce multiplayer gameplay, you will want to design the multiplayer setup menu for your game. This allows the player to select the connection and IP address they want to use. As mentioned earlier, this tutorial will focus on internet and LAN gameplay, so the sample code below will set the connection for this gameplay by default. But you can let the player select the connection method themself, if desired. DarkBasic comes with a good example to show you how to do this using the SET NET CONNECTION command. For our example, we will set the connection type to use the TCP/IP option for DirectPlay (which can be used for internet or LAN gameplay). Here is some sample code on how to do this:


rem sample code to setup a net connection
perform checklist for net connections
sleep 50
nettotal=checklist quantity()
for a = 1 to nettotal
rem find the connection for TCP/IP internet
if checklist string$(a)="Internet TCP/IP Connection For DirectPlay" then netsel=a
next a

So we have found the Internet TCP/IP option and set the "netsel" variable to the numerical value of that connection. The next step is to set the IP address for the connection. An IP address is like a phone number that allows multiple computers to talk to each other through a single connection. The HOST must be the first player to set the connection through their own IP address. Other players must then connect to the HOST through the same IP address.
If you attempt to set a connection without specifying an IP address, the user will be prompted to enter an address in a pop-up dialogue box (which may cause problems on some systems if you are not running your program in a windowed mode). It is generally best to use your own routine to prompt the user for an IP address. If you would like to automatically detect the active IP address for the HOST, I recommend using the DLL LagMaster (www.lagmaster.co.uk) offers. I suggest only using the DLL to enter the IP in initially, but let the players alter it themselves since the detected IP may not be the one they wish to use (especially if they are a player trying to join someone else's game or use a router). Once the player has entered the IP address correctly, you will set the net connection using the following command (for this example, add$ represents the IP address):


rem add$ is the IP address, example "127.0.0.7"
set net connection netsel,add$

Once this is done, the connection has been established. The next step is to decide whether the player is the HOST or a CLIENT. It's a good idea to scan for active games on the IP address at this point first, since the exact time the HOST starts a game may be unknown. For our example here, we will limit the IP address to just one game. But you can run multiple games on one IP. To search for an active game, you can use a routine such as this:


rem check for active games on the IP address the player just connected to
perform checklist for net sessions
if checklist quantity()<1 then game$="None":goto nogame
game$=checklist string$(1)
nogame:

So using this routine, if a game is detected, the "game$" variable is given the name. If no game is detected, the "game$" variable is set to "None" and moves on. If "game$" equals "None" you can prompt the player to be the HOST if they want to. Or they can re-check the list periodically if they want to wait for a game to be started (just run the routine again for each time they want to search for games). If the player chooses to be the HOST, it's time to create the game, so run a command such as this:


create net game "My Game's Name Here","Host's Name Here",8,1

The 8 is the maximum number of players allowed, 1 is for the peer-to-peer mode. If you plan to offer gameplay over an internet connection, 8 is a good standard as it can be managed acceptably by most systems and connection types. Remember, when you run your game in a multiplayer mode over an internet connection, you are expecting the player's computer to run your game, the internet service provider software they use, and the DirectPlay interface, all at once. This takes a lot more resources from the player's computer then just running your game in a single player mode. Many players still use 56K modems which may not be friendly to rapid packet exchanges with a lot of players. AOL users are forced to connect through a slow network, which further harms gameplay speed. It is best to offer wide compatibility and stability rather then risking a collapse of the connection or extremely "laggy" gameplay. If your game is more of a turn based system where packets might be sent every few seconds instead of several packets per second, you can probably bump up the total player count substantially (there is a maximum of 256). You can also allow more players if everyone uses a high-speed connection or is connected through a LAN. The choice is up to you as the designer, just remember that it means fewer people will be e-mailing you with technical support problems if more people can play effectively.

For CLIENTS, the game should be listed in the "game$" variable before they are able to join. Once a player is ready to join, use a command such as this:


join net game 1,"My Player's Name Here"

1 is the game's number. Once joined, you can now begin to exchange data. It might be a good idea to have a chatroom routine setup once the game is started by the HOST, this way players can talk to each other before starting the game session. Various game settings can be exchanged at this point along with a check of player names. You can list the names of players in one part of the screen as they join. To do this, continually run a list check of players like this:

rem get a list of all players
perform checklist for net players
for i = 1 to checklist quantity()
name$=checklist string$(i)
text 10,10+(i*20),name$
next i
This routine will give you an up-to-date listing of all players and will stay current with the list as players join or leave. If the HOST ever leaves, it is usually a good idea to close the game since the connection may eventually be lost when the HOST leaves. So it's also a good idea to notify the other players by a short message when the HOST quits, the game can then close for each player.


Section 3: Exchanging Packets

Now that you've configured your connection and started a game session, it's time to exchange information. For this example, I will stick to exchanging strings only to keep it simple, but you can exchange many data types. When a player joins the game, it might be a good idea to send a greeting automatically by using:


send net message string 0,"[My Player's Name Here] has joined the game!"

0 is used to transmit the message to every other player in the game. When the other players receive the message, you can simply display it on the screen. To check for new messages, use a routine such as this:


rem search for new messages
get net message
message$=net message string$()

The GET NET MESSAGE command sets the starting point of getting net messages at the oldest message not received. If "message$" equals anything other then "", you can then choose to display the information on the screen. It's also a good idea to loop the routine a few times if more then one message is available, that way there isn't a backup of information that might cause lag or stuttering gameplay. To do this, the routine can look like:


rem search for new messages
keepgoing:
get net message
message$=net message string$()
rem ENTER WHATEVER YOU WANT TO DO WITH THE MESSAGE$ VARIABLE HERE
rem for this example, I'll just paste it to the screen
if message$<>"" then gotmessage$=message$
text 10,100,gotmessage$
rem then check for any remaining messages
if net message exists()=1 then goto keepgoing

Remember to use whatever system settings and SYNC rates you want and adjust the routine and command placements as needed. You can also use scrolling text for chatrooms to enhance the exchange. Use this concept to exchange game parameters or any other data you want. Once everything is ready to go, it's time to move into the gameplay.


Section 4: Time For Gaming!

Using the methods above, it's pretty easy to see how information can be exchanged for turn-based gameplay, or chat exchanges, but what about fast paced real time games where the action is immediate and accuracy critical? This is where some careful designing needs to be done. There are some limitations and problems that can arise, and there is never a guarantee that severe bugs or crashes won't occur. This is especially true when players connect using all kinds of different internet connections ranging from 28K speeds to Cable modems. When in doubt, slow everything down for everyone until stability is reached.

First, there is no automatic exchange monitoring in DarkBasic. You will need to establish an integrity checking system yourself. Integrity checking can help prevent pauses and crashes. Your game will crash or pause for long periods if you try to send too many packets which can't be processed quickly enough because of various connection speeds. Even over a LAN, more then about 15 packets per second can cause lag problems if there are more then just a few players. Internet gaming might lag if more then 4 packets are sent per second. Lag is simply a hesitation in gameplay caused by more information being transmitted faster then the connection can handle it. Here are some general rules to follow to help prevent crashes and lag:

- Never insert a SLEEP command in your main gaming routine. Pausing like this means your receiving routine can't check for incoming packets, causing a backlog. It also means a packet may get lost as they are discarded by DirectPlay if delayed.

- Never transmit more packets then you receive from any one player.

- Always check the time it takes for packets to arrive from other players. When in doubt, never transmit packets at a rate faster then you receive packets from the slowest player.

- Always check for incoming messages in your main game loop, even when in menus.

- Never attempt to send more then one packet per SYNC. You can't do it anyway and should never need to since all data should be contained in a single string packet (for our example).

You can monitor the speed everyone is broadcasting at by checking the "ping." Ping is simply the time it takes for you to transmit a packet and receive a reply. It is measured in MS, or milli-seconds. You can check the ping a number of ways. One way is to transmit a time stamp from the HOST. To do this, simply use the following routine on the HOST's machine:


rem get HOST's timer
mytime=timer()
rem since ping should never be over 9,999 anyway, just get the last 4 digits of the timer
rem to minimize packet size
mytimer$=right$(str$(mytime),4)
rem PC is for ping check, the receiving routine should indentify this
send net message string 0,"PC="+mytimer$

Then the receiving computer can return the information:


rem remember message$ equals receiving information
gottime$=""
if left$(message$,3)="PC=" then gottime$=right$(message$,len(message$)-3)
if gottime$<>"" then send net message string 0,"RPC="+"My Player's Name Here"

When the HOST machine receives the "RPC=" label, it should know it received a reply from a specific player indentified by their name. You can also use number values to indentify each player (since some players might use the same name), if you wish. When the reply is received by the HOST system, it can compare the time it originally transmitted the ping check with the current time to get the ping exchange time for that specific player (current timer() minus original send timer() will equal the ping for that player). You can then store the ping rates for each player in an array. You can now transmit data packets at an average ping rate of all players (which I've had pretty good success with), or select the slowest ping rate and transmit at that rate (safer, but slows the game for all players).

This is the basic concept for speed checking. As the HOST, you can send out ping checks every few seconds to always maintain a running tab of the connection quality you have with each player. The connection quality will change, so you always need to be checking. If one player doesn't reply or slows down considerably, you will need to slow the exchange rate or stop it altogether for a second or two until quality improves (simply revert to sending test pings every 1-2 seconds or so until you get acceptable results and can resume the exchange). Don't hammer your routine with chronic ping checks. Remember, every time you send a ping packet, you lose the opportunity to send a data packet. I recommend checking ping rates like this ONLY in a menu or other area of your game where interactive gameplay is not involved. And any time a packet is transmitted, you run the risk of losing a packet sent from another player at about the same time. It's similar to two people trying to talk over the phone at the same time. So to help prevent lost packets, you may want to send each packet 2 or 3 times to verify delivery (space each duplicate packet tranmission by about 400ms or more for internet connections).

Another option is to time stamp the end of all packets from the HOST. This way, you never have to miss a data packet to check ping. The drawback is packet sizes will increase. But in my opinion, the tradeoff is well worth it. Simply use the last 4 digits of every packet for the last 4 digits of the timer() as illustrated above. This way, the host automatically receives the player ID data in addition to the time stamp, without having to transmit two seperate packets (one for the ping check and one for data).

Such speed checks need to control exchange rates for all players. When the host sends a packet, the other players can reply. When the HOST stops transmitting, players don't reply. This way, there is never a time when packets start to overload what the connections can handle. See the next section for detecting and managing lag. Another way to handle speed control is to simply put a ceiling of about 500ms for a maximum time delay between transmitted packets. That is, if 500ms has been reached and no reply has occured, send a packet anyway as a test. Since packets are frequently lost, especially over an internet connection, it is a good idea to setup a maximum time delay.

If you don't want to have the hassle of dealing with the complexities of integrity checking, here are a few guidlines to keep in mind when designing your system:

- For internet gameplay with players using a variety of connection speeds, keep packet exchanges at around 4 per second (250ms) with 4-6 players or less.

- For high speed internet connections (all players), you can generally use an exchange rate of about 5-8 packets per second.

- For LAN connections, 8-15 packets per second generally works well.

- Adding more players generally means reducing the packets per second.


Section 5: Packet Design and Gameplay Control

Designing your packets is critical to effective multiplayer. The basic concept is to keep the packet size as small as possible and only transmit the information that is important. It's a good idea to keep this concept:

SAMPLE PACKET TEMPLATE:

player's id information + player's X position + player's Y position + player's Z position + player's X direction + player's Y direction + player's Z direction + player's speed + weapon data + timestamp data for HOST

So the player sending their data would have a packet string that looks like this (using "*" as a divider between each value):

packet$=playername$+"*"+str$(camera position x())+"*"+str$(camera position y())+"*"+str$(camera position z())+"*"+str$(camera angle x())+"*"+str$(camera angle y())+"*"+str$(camera angle z())+"*"+speed$+"*"+fireweapon$+"*"+ping$
The receiving player would then see something like this in the received string when the packet is detected:

"Bill10424*543.1122*58.2311*801.8790*44.3250*172.0093*94.2210*15*0*2480"
The received string can then be broken down within the program using the "*" dividers and assign the values to the needed variables. This can be done by getting each digit using the mid$() command until the value equals "*", then advance to the next needed value.

Keeping this information in a single packet will help optimize accuracy while reducing lag or jittering gameplay. You can seperate the various bits of information with a marker, such as "," or "*" and then the receiving computer can break it down and use the variables as needed. It is ALWAYS faster to breakdown a single packet within the program then it is to try and send multiple packets for various gamestate parameters. The timestamp at the end can be returned to the HOST in the same form, then the HOST machine can use the playerid and timestamp value to quickly establish an accurate ping.

Sending seperate packet types for weapon data and player data will mean longer delays between updates for various aspects of gameplay. It's best to keep as much information as possible in a single packet.

For chat exchanges during gameplay, you can simply use a format like this:


send net message string 0,"M=[Enter Message Data Here]"

Then all receiving computers can detect the message and display it as needed.

You will lose packets. Over any connection (including LAN) packets will be lost. DarkBasic offers a packet guarantee option for some data types, which would be a good idea to use for message exchanges to make sure the message arrives. It is not recommended you use packet guarantees for game data packets as it adds considerable delay to the exchange rate (in some cases, as much as a 300 MS additional delay or more). If you choose to just use regular packet exchanges, consider sending two or three copies of the same message to help insure its arrival. Make sure your program accounts for lost packets and can adjust a scene accordingly.

Your game will need to predict movement between packets for each player. This is critical in first person shooters and flight simulations. To do this, create a routine that compares the rate of movement (rotation and direction) between received packets for each player. For direction movement, simply move the player at the previously received speed (that's the easy part). For rotation movement, divide the total rate of movement between the last two received packets by the number of frames counted between those two received packet, or use the ping rate divided by the time between each frame, example: 30 frames-per-second equates to about 33 MS per frame, so if the ping is 300, the movement rate would be TOTALXMOVE/(300/33). This way, you now how far to continue rotating the player for each frame of gameplay. This is why it is important to standardize your framerate for every player, or at least have a set rate of motion for the scene, regardless of framerate. You will need to fine tune the routine to get it the way you want. And you may want to add a 0.9 or so multiplication factor to the movement speeds to gradually slow it down, this will help prevent jerky movement when packets are finally received.

Sound a bit tricky? Let's try an example just using the Y axis of rotation. Remember, first person shooters typically use Euler angles (the ROTATE OBJECT command set) while flight simulations typically use Quaternion angles (the TURN/ROLL/PITCH command set). You will need to adapt your rotation set with the multiplayer routine. For this example, we will use the Y angle of rotation, commonly used for turning left or right in many first person shooter games. First we will assume we've received a packet from the other player giving us the initial Y angle for the object that represents the other player. Let's set this value to the variable OLDY (remember to use an array for more then one other player, so it might read OLDY(I)). In your routine, you will continually set OLDY to the last received Y angle from each player, then set the variable NEWY to the Y angle you just received. So in our example, OLDY now represents the original Y angle we received from the first packet, and we've just received a second packet, which we set the Y angle value to NEWY. Let's say OLDY has a value of 34.5 and NEWY has a value of 42.0. It's obvious that the player has turned right, since going the other direction would mean they turned 352.5 degrees in a very short time, which should be an impossibility in your first person shooter. So we now know the direction they turned without having to receive any extra data to find out. The total distance travelled was 7.5 degrees (42.0-34.5), which means we know that the player made a controlled movement of 7.5 degrees to the right in the time it took to receive two packets. We have the distance, now we just need the time. Hopefully, you've been tracking the ping rate for the player also, as this will help you predict the future movement of the player. In our example, the ping rate for this player was 288, just over 1/4 of a second. Our game's frame rate is 30, which means each frame takes 33.33 ms of time. Using our formula above, we know that at 33.33 ms for each frame, our moving player took nearly 9 frames of gameplay to turn 7.5 degrees to the right (288 divided by 33.33). So if we want to predict this player's future movement, we can guess they will likely continue turning right. We then take the 7.5 degrees of motion and divide it by the number of frames it took to get there, 9. Our result is 0.83333. So for the next 9 frames we can continue to turn this player's object in our scene by 0.83333 degrees each frame. Then if we get the next packet in about the same time, 288 ms, and the player continues to turn at the same rate, it will appear that the player smoothly turned in that direction, instead of suddenly jerking 7.5 degrees to the right. This means movement will be smoother, aim will be more accurate, and gameplay more enjoyable.

You will likely need to fine tune this concept to control twitching, so slowly reducing the automated movement might be a good idea. You will also need to account for the 360 degree barrier. That is, when an object rotates from a Y angle of 5 to 355, the distance is only 10 degrees to the left, not 350 degrees to the right. Use the wrapvalue option and "y>180 then y=y-360" formulas to adjust as needed.


L A G

The scurge of multiplayer gaming. One of the things you will have to contend with is lag, also known at latency. This problem is caused by overloading the gameplay with too many packets. When it happens, your game will pause until the packet backlog is processed. This may take 1/10 of a second, or it may take 5 seconds, depending on how many packets you've tried to send. Your job as the designer is to detect when this happens and respond by haulting packet broadcasting. One way to detect lag is to keep track of the time between frames. Keep in mind, some people's machines will be able to run your game at a higher FPS (frames per second) then others, so you'll want to only detect a frame delay of say 150-250 ms. Here's a quick routine to check the time between frames. Remember, we are using a framerate of 30 in our example, so this routine is designed to detect a frame delay of roughly 200 ms.


rem LAG CHECKER
lag=0
oldtimerms=timerms
timerms=timer()
rem if more then a 200 ms delay between frames, hault packet sending
if timerms-oldtimerms>200 then text 10,10,"LAG OCCURING":lag=1
rem then when the lag variable equals 1, don't transmit packets for 1000 ms or so

When the LAG variable equals 1, stop sending packets for about 1 second or more. Continue running your GET NET MESSAGE routine to clear out the remaining packets that caused the lag. 1 second is usually enough time to clear the packets with 4 or fewer players. Then as the signal quality improves, you will be able to resume sending packets. If the signal quality continues to be poor, the routine will repeat itself until the signal is clean again.


Final Notes:

By using the concepts in this tutorial, you will be able to create a basic multiplayer system that works with a real-time game. The design and fine-tuning is up to you. The key to creating a solid multiplayer system is finding the balance between sending too much information and not sending enough. Sending too much means your game will lag, send too little and the gameplay will be jerky. It's best to always check the speed that messages are travelling at, since the signal quality will go up and down for each player. Also remember to use commands such as NET GAME NOW HOSTING and NET GAME LOST to continually check if the session assigns a new host or is lost entirely. If a new host is assigned, then the host has left or lost connection and it's usually best to close the game and return the players to a chatroom.

If you'd like to see how I've implemented some of these concepts in a game I created, check out Star Wraith 3 at www.starwraith.com and you will see the basic outline illustrated here. The game starts by setting up the connection and establishing a game session. It then moves into a scrolling chatroom sequence, a menu mode, then gameplay. In every area of the game, there is a chatbox or chat option for players to talk to each other, which is very important.

That's all for now, good luck with your projects